package com.firesyde.gwt.persistence.bdb;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;

import com.firesyde.gwt.persistence.IDataManager;
import com.firesyde.gwt.persistence.client.StorableDAO;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.sleepycat.bind.EntryBinding;
import com.sleepycat.bind.serial.ClassCatalog;
import com.sleepycat.bind.serial.SerialBinding;
import com.sleepycat.je.Cursor;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;

//XXX does this ensure that it's a singleton PER TYPE?
@Singleton
public class BerkeleyDBDataManager implements IDataManager {
	
	//used for persistence
	@Inject protected Database db;
	@Inject protected Environment env;
	@Inject protected ClassCatalog catalog;
	@Inject Logger logger;
	
	private EntryBinding beanBinding = null; 
	private EntryBinding keyBinding = null;
	
	public BerkeleyDBDataManager() {
		keyBinding = new SerialBinding(catalog, Long.class);
	}
	
	/* (non-Javadoc)
	 * @see com.firesyde.gwt.persistence.IDataManagerIFace#close()
	 */
	public void close() {
		try {
			catalog.close();
			db.close();
		} catch (DatabaseException e) {
			System.out.println("Error while closing database: "+e.getMessage());
			e.printStackTrace();
		}
	}
	
	/* (non-Javadoc)
	 * @see com.firesyde.gwt.persistence.IDataManagerIFace#exists(java.lang.String)
	 */
	public boolean exists(String key) {
		OperationStatus result = null;
		try {
			result = db.get(null, new DatabaseEntry(key.getBytes("UTF-8")), new DatabaseEntry(), LockMode.DEFAULT);
		} catch (Exception e) {
			logger.error("Error checking whether record "+key+" exists: "+e.getMessage());
			e.printStackTrace();
			return false;
		} 
	    
	    // Perform the get.
	    if (result ==
	        OperationStatus.SUCCESS) {
	    	return true;
	    } else {
	    	return false;
	    } 
	    
	}
	
	/* (non-Javadoc)
	 * @see com.firesyde.gwt.persistence.IDataManagerIFace#store(com.firesyde.gwt.persistence.AbstractStorableDAO)
	 */
	public <T extends StorableDAO> void store(T record) {
		try {
			
			if(record==null) return;

			// Create the binding - basic java serialisation
			
			//lazy creation of beanbinding - we can't create it until we have a non-null object instance
			//XXX TODO - now that our datamanagers manage any type, this needs to be a hash or something
			if(beanBinding==null)
				beanBinding = new BeanBinding(record.getClass());

			// Create the DatabaseEntry for the key
			DatabaseEntry theKey = new DatabaseEntry();

			keyBinding.objectToEntry(record.key(), theKey);
			
			// Create the DatabaseEntry for the data. Use the EntryBinding object
			// that was just created to populate the DatabaseEntry
			DatabaseEntry theData = new DatabaseEntry();
			beanBinding.objectToEntry(record, theData);

			// Put it as normal
			db.put(null, theKey, theData);
			
			env.sync();
			
			System.out.println("Wrote record with key "+record.key());
		} catch (Exception e) {
			System.out.println("Error while writing record into the database: "+e.getMessage());
			e.printStackTrace();
		}
	}
	
	/* (non-Javadoc)
	 * @see com.firesyde.gwt.persistence.IDataManagerIFace#fetchAll(java.lang.Class)
	 */
	public <T extends StorableDAO> List<T> fetchAll(Class<T> fetchClass) {
		
		//XXX don't create these every time - cache according to types?
//		EntryBinding keyBinding =
//            new SerialBinding(catalog, String.class);
//		EntryBinding valueBinding =
//            new BeanBinding(fetchClass);
//		
//		StoredMap myMap =
//            new StoredMap(db,
//            		keyBinding, valueBinding, true);
//		StoredEntrySet es = (StoredEntrySet) myMap.entrySet();
//		
//		return new ArrayList<T>(myMap.values());
		
		if(beanBinding==null)
			beanBinding = new BeanBinding(fetchClass);
		
		ArrayList<T> allObjects = new ArrayList<T>();
		
		try {
			Cursor cursor = db.openCursor(null, null);
			DatabaseEntry foundKey = new DatabaseEntry();
		    DatabaseEntry foundData = new DatabaseEntry();
		 // To iterate, just call getNext() until the last database record has been 
		    // read. All cursor operations return an OperationStatus, so just read 
		    // until we no longer see OperationStatus.SUCCESS
		    while (cursor.getNext(foundKey, foundData, LockMode.DEFAULT) ==
		        OperationStatus.SUCCESS) {
		        // getData() on the DatabaseEntry objects returns the byte array
		        // held by that object. We use this to get a String value. If the
		        // DatabaseEntry held a byte array representation of some other data
		        // type (such as a complex object) then this operation would look 
		        // considerably different.
		        String keyString = new String(foundKey.getData(), "UTF-8");
		        String dataString = new String(foundData.getData(), "UTF-8");
		        
		        System.out.println("Key | Data : " + keyString + " | " + 
	                       dataString + "");
		        
		        System.out.println("Data size: "+foundData.getSize());
		        
		        if(foundData.getSize()>1)
		        	allObjects.add((T) beanBinding.entryToObject(foundData));
		
		    }
		    cursor.close();
		} catch (Exception e) {
			logger.error("Couldn't read records: "+e.getMessage());
			e.printStackTrace();
		}
		
		return allObjects;
	}
	
	/* (non-Javadoc)
	 * @see com.firesyde.gwt.persistence.IDataManagerIFace#fetch(java.lang.String, java.lang.Class)
	 */
	public <T extends StorableDAO> T fetch(String[] keys, Object[] values, Class<T> fetchClass) {
		//XXX TODO exceptions!
		if(keys.length!=values.length) {
			logger.error("number of keys must match number of values ("+keys.length+"!="+values.length+")");
			return null;
		}
			
			
		if(keys.length>1 || values.length>1) {
			logger.error("BDB Data Manager only supports one key!");
			return null;
		}
		
		String key = (String)values[0];
			
		try {
		    // Create a pair of DatabaseEntry objects. theKey
		    // is used to perform the search. theData is used
		    // to store the data returned by the get() operation.
		    DatabaseEntry theKey = new DatabaseEntry(key.getBytes("UTF-8"));
		    DatabaseEntry theData = new DatabaseEntry();
		    
		    //BeanBinding automagically maps database entries to bean properties,
		    //leaving our GWT DAO delightfully Java5-free
		    EntryBinding beanBinding = new BeanBinding(fetchClass);
		    
		    OperationStatus result = db.get(null, theKey, theData, LockMode.DEFAULT);
		    
		    // Perform the get.
		    if (result ==
		        OperationStatus.SUCCESS) {
		    	T retrievedData = (T)beanBinding.entryToObject(theData);
		    	return retrievedData;
		    } else {
		        System.out.println("No record found for key '" + key + "': "+result.toString());
		    } 
		} catch (Exception e) {
			System.out.println("Error while getting stuff out of the database: "+e.getMessage());
			e.printStackTrace();
		}
		return null;
	}
}
